__author__ = 'Bao Liu'

import string

class JsonParserDecoder(object):
    escape_set = set(['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u'])

    @staticmethod
    def __next_idx_skip_whitespace(s, idx):
        try:
            while True:
                if s[idx] not in string.whitespace:
                    break
                idx += 1
        except IndexError:
            pass

        return idx

    @classmethod
    def __detect_invalid_escape(cls, s):
        idx = 0

        while idx < len(s):
            idx = string.find(s, '\\', idx)
            if idx == -1:
                return True

            cnt = 0
            while idx < len(s):
                if s[idx] == '\\':
                    cnt += 1
                    idx += 1
                else:
                    break

            if cnt % 2 != 0:
                if idx >= len(s) or s[idx] not in cls.escape_set:
                    err_msg = JsonParserHelper.make_err_detail('Invalid escape character', s, idx)
                    raise ValueError(err_msg)


    @staticmethod
    def parse_string(s, idx):
        """
        Parse one string from idx to string(ascii) end and convert result to unicode.
        s = json string to be parsed
        idx = the index of s after '"'
        return result string(unicode) with escape character decoded
        """
        end = idx
        while True:
            end = string.find(s, '"', end)
            if end == -1:
                err_msg = JsonParserHelper.make_err_detail('Expecting value or "', s, idx)
                raise ValueError(err_msg)

            cnt = 0
            it = end - 1
            while it >= idx:
                if s[it] == '\\':
                    cnt += 1
                    it -= 1
                else:
                    break

            if cnt % 2 == 0:
                break
            else:
                end += 1

        # Translate '\/' for it can't be decoded by unicode_escape
        result = s[idx:end]
        pos = 0
        while True:
            pos = string.find(result, '/', pos)
            if pos == -1:
                break

            cnt = 0
            it = pos - 1
            while it >= 0:
                if result[it] == '\\':
                    cnt += 1
                    it -= 1
                else:
                    break

            if cnt % 2 != 0:
                result = result[:pos - 1] + result[pos:]
            else:
                pos += 1

        JsonParserDecoder.__detect_invalid_escape(result)

        return result.decode('unicode-escape'), end + 1

    @staticmethod
    def parse_object(s, idx, scan_once):
        """
        Parse one dict from idx to string(ascii) end.
        s = json string to be parsed
        idx = the index of s after '{'
        scan_once =  JsonParserDecoder.__scan_once
        return result dict
        """
        pairs = []
        idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)
        nextchar = s[idx:idx + 1]

        if nextchar == '}':
            return {}, idx + 1
        elif nextchar != '"':
            err_msg = JsonParserHelper.make_err_detail('Expecting property name enclosed in double quotes', s, idx)
            raise ValueError(err_msg)

        # Assert s[idx] == '"'

        while True:
            # Parse property
            key, idx = JsonParserDecoder.parse_string(s, idx + 1)
            # Parse ':'
            if s[idx:idx + 1] != ':':
                idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)
                if s[idx:idx + 1] != ':':
                    err_msg = JsonParserHelper.make_err_detail("Expecting ':'", s, idx)
                    raise ValueError(err_msg)
            # Parse value
            idx += 1
            value, idx = scan_once(s, idx)
            pairs.append((key, value))

            # Skip whitespace
            idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)

            # If }, exit loop
            nextchar = s[idx:idx + 1]
            if nextchar == '}':
                break
            elif nextchar != ',':
                err_msg = JsonParserHelper.make_err_detail("Expecting ',' or '}'", s, idx)
                raise ValueError(err_msg)

            # Else, find next property string's '"'
            idx += 1
            idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)
            if s[idx:idx + 1] != '"':
                err_msg = JsonParserHelper.make_err_detail('Expecting property name enclosed in double quotes', s, idx)
                raise ValueError(err_msg)

        return dict(pairs), idx + 1


    @staticmethod
    def parse_array(s, idx, scan_once):
        """
        Parse one list from idx to string(ascii) end.
        s = json string to be parsed
        idx = the index of s after '['
        scan_once = JsonParserDecoder.__scan_once
        return result list
        """
        values = []
        idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)
        nextchar = s[idx:idx + 1]

        if nextchar == ']':
            return values, idx + 1
        elif nextchar == '':
            err_msg = JsonParserHelper.make_err_detail("Expecting value or ']'", s, idx)
            raise ValueError(err_msg)

        while True:
            # Parse value
            value, idx = scan_once(s, idx)
            values.append(value)

            # Skip whitespace
            idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)

            # If ], exit loop
            nextchar = s[idx:idx + 1]
            if nextchar == ']':
                break
            elif nextchar != ',':
                err_msg = JsonParserHelper.make_err_detail("Expecting ',' or ']'", s, idx)
                raise ValueError(err_msg)

            # Else, find next value begins
            idx += 1
            idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)

        return values, idx + 1

    @staticmethod
    def parse_number(s, idx):
        """
        Parse a number from idx to string(ascii) end.
        s = json string to be parsed
        idx = the index of s
        return result number(int/long/float)
        """
        def _parse_token(s, idx):
            numeric_charset = '0123456789+-eE.'
            end = idx

            try:
                while True:
                    if s[end] not in numeric_charset:
                        break
                    end += 1
            except IndexError:
                pass

            if idx != end:
                return s[idx:end], end
            else:
                return None, None

        token, end = _parse_token(s, idx)
        if token is not None:
            try:
                return int(token), end
            except:
                pass
            try:
                return float(token), end
            except:
                pass

        raise ValueError(JsonParserHelper.make_err_detail('Expecting value or number', s, idx))

    @staticmethod
    def __scan_once(s, idx):
        """
        Decode one json element(string/dict/list/int/long/float/None/True/False from idx to string(ascii) end.
        s = json string to be parsed
        idx = index of s
        return result json element
        """
        idx = JsonParserDecoder.__next_idx_skip_whitespace(s, idx)

        try:
            nextchar = s[idx]
        except IndexError:
            err_msg = JsonParserHelper.make_err_detail('Expecting value', s, idx)
            raise ValueError(err_msg)

        if nextchar == '"':
            return JsonParserDecoder.parse_string(s, idx + 1)
        elif nextchar == '{':
            return JsonParserDecoder.parse_object(s, idx + 1, JsonParserDecoder.__scan_once)
        elif nextchar == '[':
            return JsonParserDecoder.parse_array(s, idx + 1, JsonParserDecoder.__scan_once)
        elif nextchar == 'n' and s[idx:idx + 4] == 'null':
            return None, idx + 4
        elif nextchar == 't' and s[idx:idx + 4] == 'true':
            return True, idx + 4
        elif nextchar == 'f' and s[idx:idx + 5] == 'false':
            return False, idx + 5
        else:
            return JsonParserDecoder.parse_number(s, idx)

    @staticmethod
    def scan_once(s):
        return JsonParserDecoder.__scan_once(s, 0)[0]


class JsonParserEncoder(object):
    escape_map = {
        u'\\': r'\\',
        u'"': r'\"',
        u'\b': r'\b',
        u'\f': r'\f',
        u'\n': r'\n',
        u'\r': r'\r',
        u'\t': r'\t',
    }

    # JSON requires that all control characters U+0000 through U+001F be escaped
    for i in xrange(0x20):
        escape_map.setdefault(unichr(i), '\\u%04x' % (i,))

    @classmethod
    def __encode_escape(cls, s):
        """
        Encode unicode to escaped str
        """
        return ''.join(map(lambda x: cls.escape_map[x] if x in cls.escape_map else x.encode('unicode_escape'), s))

    @staticmethod
    def convert_once(item):
        if isinstance(item, dict):
            json_dict = []

            for elem in item.iteritems():
                json_elem = JsonParserEncoder.convert_once(elem)
                if json_elem != '':
                    json_dict.append(json_elem)

            return '{' + string.join(json_dict, ', ') + '}'
        elif isinstance(item, tuple):
            key = item[0]
            value = item[1]

            # omit item if key is not a str
            if isinstance(key, unicode):
                json_elem = JsonParserEncoder.convert_once(value)
                return '"' + JsonParserEncoder.__encode_escape(key) + '": ' + json_elem

            return  ''
        elif isinstance(item, list):
            json_list = []

            for elem in item:
                json_elem = JsonParserEncoder.convert_once(elem)
                if json_elem != '':
                    json_list.append(json_elem)

            return '[' + string.join(json_list, ', ') + ']'
        elif isinstance(item, unicode):
            return '"' + JsonParserEncoder.__encode_escape(item) + '"'
        elif isinstance(item, bool):
            return 'true' if item else 'false'
        elif isinstance(item, int) or isinstance(item, long) or isinstance(item, float):
            return str(item)
        elif item is None:
            return 'null'
        else:
            raise ValueError("Unknown type in dict, can't convert to json")

class JsonParserHelper(object):
    @staticmethod
    def __deep_copy(id_set, d):
        obj_id = id(d)

        if obj_id in id_set:
            raise ValueError("Found a reference cycle in dict")

        if isinstance(d, dict):
            pairs = []
            id_set.add(obj_id)

            for elem in d.iteritems():
                key = JsonParserHelper.__deep_copy(id_set, elem[0])
                value = JsonParserHelper.__deep_copy(id_set, elem[1])
                pairs.append((key, value))

            return dict(pairs)
        elif isinstance(d, list):
            items = []
            id_set.add(obj_id)

            for elem in d:
                items.append(JsonParserHelper.__deep_copy(id_set, elem))

            return items
        elif isinstance(d, str):
            return d.decode('unicode_escape')
        elif isinstance(d, unicode) or isinstance(d, bool):
            return d
        elif isinstance(d, int) or isinstance(d, long) or isinstance(d, float):
            return d
        elif d is None:
            return None
        else:
            raise ValueError("Found unknown type when deep copying")

    @staticmethod
    def deep_copy(d):
        return JsonParserHelper.__deep_copy(set(), d)

    @staticmethod
    def __check_reference_cycle(id_set, d):
        obj_id = id(d)

        if obj_id in id_set:
            raise ValueError('Found a reference cycle, please check JsonParser.__setitem__ and JsonParser.update.')

        if isinstance(d, dict):
            id_set.add(obj_id)
            # Only values can be mutable object
            for val in d.itervalues():
                JsonParserHelper.__check_reference_cycle(id_set, val)
        elif isinstance(d, list):
            id_set.add(obj_id)
            for elem in d:
                JsonParserHelper.__check_reference_cycle(id_set, elem)

    @staticmethod
    def check_reference_cycle(d):
        JsonParserHelper.__check_reference_cycle(set(), d)

    @staticmethod
    def make_err_detail(msg = '', src = '', pos = 0):
        err_msg = msg

        if src != '':
            err_msg += ':\n'
            err_msg += src[:pos + 1]

        return err_msg


class JsonParser(object):
    """ JsonParser according to <http://json.org>

    Performs the following translations in decoding by default:

    +---------------+-------------------+
    | JSON          | Python            |
    +===============+===================+
    | object        | dict              |
    +---------------+-------------------+
    | array         | list              |
    +---------------+-------------------+
    | string        | unicode           |
    +---------------+-------------------+
    | number (int)  | int, long         |
    +---------------+-------------------+
    | number (real) | float             |
    +---------------+-------------------+
    | true          | True              |
    +---------------+-------------------+
    | false         | False             |
    +---------------+-------------------+
    | null          | None              |
    +---------------+-------------------+

    """

    def __init__(self):
        self.json_dict = dict()

    def load(self, s):
        """
        Load a JSON string(encoded with ascii) to internal dict
        s = JSON string
        """
        self.json_dict = JsonParserDecoder.scan_once(s)

    def dump(self):
        """
        Dump internal dict to a JSON string(encoded with ascii)
        return JSON string
        """
        JsonParserHelper.check_reference_cycle(self.json_dict)
        return JsonParserEncoder.convert_once(self.json_dict)

    def loadJson(self, f):
        """
        Load a JSON file(encoded with ascii) to internal dict
        f = JSON file path
        """
        with open(f) as json_file:
            json_str = json_file.read()
            self.json_dict = JsonParserDecoder.scan_once(json_str)

    def dumpJson(self, f):
        """
        Dump internal dict to a JSON file(encoded with ascii)
        f = JSON file path
        """
        with open(f, 'wt') as json_file:
            JsonParserHelper.check_reference_cycle(self.json_dict)
            json_str = JsonParserEncoder.convert_once(self.json_dict)
            json_file.write(json_str)

    def loadDict(self, d):
        """
        Load a dict to internal dict(make deep copy)
        d = Python dict
        """
        self.json_dict = JsonParserHelper.deep_copy(d)

    def dumpDict(self):
        """
        Dump internal dict to a dict(make deep copy)
        return dict
        """
        return JsonParserHelper.deep_copy(self.json_dict)

    def __getitem__(self, key):
        """
        Return a shadow copy
        """
        return self.json_dict[key]

    def __setitem__(self, key, value):
        """
        Make a deep copy during assignment.
        """
        self.json_dict[key] = JsonParserHelper.deep_copy(value)

    def update(self, value):
        """
        Accept dict as parameter to update internal dict.
        """
        if isinstance(self.json_dict, dict) and isinstance(value, list):
            self.json_dict.update(JsonParserHelper.deep_copy(value))
        elif not isinstance(self.json_dict, dict):
            raise ValueError('JsonParser is not a dict, can not be updated.')
        else:
            raise ValueError('Only support dict as update parameter.')
